<?php

/**
 * MLEPP - ManiaLive Extending Plugin Pack
 *
 * -- MLEPP Plugin --
 * @name Dedimania
 * @date 29-11-2011
 * @version r1191
 * @website mlepp.trackmania.nl
 * @package MLEPP
 *
 * @author Florian "Flo" Schnell
 * @reauthored The MLEPP Team
 * @copyright 2010 - 2011
 *
 * ---------------------------------------------------------------------
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 * ---------------------------------------------------------------------
 * You are allowed to change things or use this in other projects, as
 * long as you leave the information at the top (name, date, version,
 * website, package, author, copyright) and publish the code under
 * the GNU General Public License version 3.
 * ---------------------------------------------------------------------
 */

namespace ManiaLivePlugins\MLEPP\Dedimania;

use ManiaLive\Utilities\Console;
use ManiaLib\Utils\TMStrings as String;
use ManiaLive\DedicatedApi\Connection;
use ManiaLive\Data\Storage;
use ManiaLive\Utilities\Time;
use ManiaLive\DedicatedApi\Structures\GameInfos;
use ManiaLive\DedicatedApi\Structures\Player;
use ManiaLive\Gui\Windowing\Windows\Info;
use ManiaLib\Gui\Elements\Icons128x128_1;
use ManiaLive\Gui\Handler\GuiHandler;
use ManiaLive\Event\Dispatcher;
use ManiaLive\Threading\Commands\RunCommand;
use ManiaLive\Threading\ThreadPool;
use ManiaLive\DedicatedApi\Xmlrpc\Message;
use ManiaLive\DedicatedApi\Xmlrpc\Base64;

use ManiaLivePlugins\MLEPP\Dedimania\Structures\Record;
use ManiaLivePlugins\MLEPP\Dedimania\Runnables\DedimaniaCall;
use ManiaLivePlugins\MLEPP\Dedimania\Runnables\NotificationCall;
use ManiaLivePlugins\MLEPP\Dedimania\Structures\Map;
use ManiaLivePlugins\MLEPP\Dedimania\Events\onDataSync;
use ManiaLivePlugins\MLEPP\Dedimania\Config;

use ManiaLivePlugins\MLEPP\Dedimania\Gui\Windows\listWindow;
use ManiaLivePlugins\MLEPP\Dedimania\Gui\Controls\Header;
use ManiaLivePlugins\MLEPP\Dedimania\Gui\Controls\Normal;


class Dedimania extends \ManiaLive\PluginHandler\Plugin implements \ManiaLive\Data\Listener, \ManiaLive\Features\Tick\Listener, \ManiaLive\Threading\Listener {
    protected static $code;
    public static $idleTimeout;
    public static $debug;
    
    public static $_Instance;
    
	protected $storage;
	protected $last_players_update;
	
	protected $packmask;
	protected $version_info;
	protected $version;
	
	protected $records_max;
	protected $records;
    protected $records_dedi;
	protected $records_last;
	protected $records_new;
	protected $records_by_login;
	protected $records_count;
    protected $records_top1;
	
	protected $map_previous;
	protected $map_current;
	
	protected $ready;
	protected $callback_queue;
	
	protected $thread_id;
	protected $notifier;
	protected $cp_times;
    protected $sessionId;
    
	public static $notifications;
	public static $notifyNewFirstRecord;
	public static $notifyNewRecord;
	public static $notifyImprovedFirstRecord;
	public static $notifyImprovedRecord;
	public static $notifyImprovedRecordTimeOnly;
    
    public function onInit() {
        $this->setPublicMethod('getVersion');
		$this->setVersion(260);
        
		$config = Config::getInstance();
        self::$code = $config->code;
        self::$idleTimeout = $config->idleTimeout;
        self::$debug = $config->debug;
    	self::$notifications = $config->notifications;
    	self::$notifyNewFirstRecord = $config->notifyNewFirstRecord;
    	self::$notifyNewRecord = $config->notifyNewRecord;
    	self::$notifyImprovedFirstRecord = $config->notifyImprovedFirstRecord;
    	self::$notifyImprovedRecord = $config->notifyImprovedRecord;
    	self::$notifyImprovedRecordTimeOnly = $config->notifyImprovedRecordTimeOnly;
    }
    
    public function onLoad() {
		$this->setBusy();
		$this->records = array();
        $this->records_dedi = array();
		$this->records_new = array();
		$this->records_by_login = array();
		$this->callback_queue = array();
		
		// get storage reference ...
		$this->storage = Storage::getInstance();		
		
		// gather some information ...
        
		$this->version_info = Connection::getInstance()->getVersion();
		
		$this->enableThreadingEvents();
		$this->enableDedicatedEvents();
		$this->enableTickerEvent();
		$this->enableStorageEvents();
        
        $cmd = $this->registerChatCommand('dedirecs', 'dedirecordsChat', 0, true);
        $cmd->help = 'Shows the Dedimania Records for this track.';
		
		// not yet ...
		$this->last_players_update = time() + self::$idleTimeout;
		
		// start worker thread ...
		$this->thread_id = ThreadPool::getInstance()->createThread();
		
		// register chat command for displaying records ...
		/*$cmd = $this->registerChatCommand('dedimania', 'showRecordWindow', 0, true);
		$cmd->help = 'displays window with dedimania rankings for the current challenge.';*/
		
		// check if password has been set in the config file
		if(self::$code == null) {
			throw new ConfigurationException('You need to set the ManiaLivePlugins\MLEPP\Dedimania\Config.code option in the config.ini!');
        }
		
        self::$_Instance = $this;
        
		// connect for the first time ...
		$this->connectToDedimania();
	}
    
    public static function getInstance() {
        return self::$_Instance;
    }
    
    public function dedirecordsChat($login) {        
        if(!empty($this->records)) {
            $window = listWindow::Create($login);
      		$window->setSize(180, 95);
      		$window->clearAll();
            $window->setTitle('Dedimania Records for this track');
      		// prepare cols ...
      		$window->addColumn('Rec', 0.1);
      		$window->addColumn('Time', 0.3);
            $window->addColumn('Nickname', 0.4);
            $window->addColumn('Link', 0.2);
        
      		// refresh records for this window ...
      		$window->clearItems();
      		$id = 1;
      		foreach($this->records as $record) {
      		    //Console::println('### [Debug] [Dedimania] Record #'.$id.':');
      		    //print_r($record);
                if($id < ($this->records_max+1)) {
                    $login = (is_array($record)) ? $record['Login'] : $record->login;
     			    $entry = array
         			(
        				'Rec' => $id,
        				'Time' => (is_array($record)) ? Time::fromTM($record['Best']) : Time::fromTM($record->best),
                        'Nickname' => (is_array($record)) ? $record['NickName'] : $record->nickName,
                        'Link' => '$l[http://dedimania.net/tm2stats/?do=stat&Login='.$login.'&UId='.$this->storage->currentMap->uId.'&Show=RECORD]Click$l'
         			);
         			$id++;
         			$window->addItem($entry);
                }
      		}
        
      		$window->centerOnScreen();
      		$window->show();
        } else {
            $this->connection->chatSendServerMessage('$fff»»$093This track does not have any Dedimania Records yet!', $login);
        }
    }
    
   	public function onThreadRestart($thread) {
   	    if(self::$debug) $this->writeConsole('DM thread died/crashed (check threading logs for more info). Attempting to reconnect..');
		if($thread->getId() == $this->thread_id) {
			// begin connection to dedimania ...
			$this->connectToDedimania();
		} elseif(self::$debug) {
            $this->writeConsole('Error!! Thread IDs do not match!');
        }
	}
    
    function onThreadStart($thread) {
        if(self::$debug >= 2) $this->writeConsole('Starting new thread..');
    }
       
    function onThreadDies($thread) {
        if(self::$debug) $this->writeConsole('Dedimania thread died!');
    }
       
    function onThreadTimesOut($thread) {
        if(self::$debug) $this->writeConsole('Dedimania thread timed out!');
    }
    
	/*
	 * Test if a login is a LAN one, ie finishing with  _xxx.xxx.xxx.xxx_xxx)
	 */
	function is_LAN_login($login) {
		$num = '(25[0-5]|2[0-4]\d|[01]?\d\d|\d)';
		if(preg_match("/(_{$num}\\.{$num}\\.{$num}\\.{$num}_\d+)$/", $login) > 0)
			return true;
		return false;
	}
	
	public function connectToDedimania() {
		$this->writeConsole('Starting authentication on server ...');
		
		$call = new DedimaniaCall();
    
		$server = $this->connection->GetDetailedPlayerInfo($this->storage->serverLogin);
		$this->packmask = 'Canyon';
		// try to open session at auth server ...
		$params = array
		(
		 'Game' => 'TM2',
		 'Login' => $this->storage->serverLogin,
		 'Code' => self::$code,
		 'Path' => $server->path,
		 'Packmask' => $this->packmask,
		 'ServerBuild' => $this->version_info->build,
		 'ServerVersion' => $this->version_info->version,
		 'Tool' => 'ManiaLive2',
		 'Version' => \ManiaLiveApplication\Version.'-dev'
		 );
		$call->addRequest(new Request('dedimania.OpenSession', array((object)$params)));
		
		
		
		// send request ...
		$this->writeConsole('Please wait, opening session ...');
		$command = new RunCommand($call, array($this, 'cbConnectToDedimania'));
		ThreadPool::getInstance()->addCommand($command, $this->thread_id);
	}
    
    public function cbConnectToDedimania($command) {
		// okay process got server's response!
		$this->writeConsole('Done!');
		
        if(isset($command->result[0]['errors']) && $command->result[0]['errors'] != '') {
            $this->writeConsole($command->result[0]['errors']);
        }

		// we check the authentication query for success ..
		if(isset($command->result[0]['SessionId']) && $command->result[0]['SessionId'] != "") {
            $this->sessionId = $command->result[0]['SessionId'];
			$this->writeConsole('Successfully authenticated!');
            if(self::$debug >= 3) {
                echo '[DM-DEBUG] Request Result:/n';
                print_r($command->result);
            }
		} else {
            if(self::$debug >= 2) {
                echo '[DM-DEBUG] Connection failed..  Result:/n';
                print_r($command->result);
            }
			//print_r($command->result);
			die("[".date('H:i:s')."|Dedimania] Error: Could not connect to Dedimania, authentication failed:\n".$command->result[0]['Error']);
		}
        
		$call = new DedimaniaCall();
		// validate account to be sure ...
		$call->addRequest(new Request('dedimania.CheckSession', array($this->sessionId)));
		
		// send request ...
		$this->writeConsole('Please wait, checking session ...');
		$command = new RunCommand($call, array($this, 'cbConnectToDedimania2'));
		ThreadPool::getInstance()->addCommand($command, $this->thread_id);
    }
    
    public function cbConnectToDedimania2($command) {
        if(isset($command->result[0]['errors']) && $command->result[0]['errors'] != '') {
            $this->writeConsole($command->result[0]['errors']);
        }

        // we check the authentication query for success ..
        if($command->result[0] == true) {
            $this->writeConsole('Account is valid!');
            if (self::$debug >= 3) {
                echo '[DM-DEBUG] Request Result:/n';
                print_r($command->result);
            }
        } else {
            if(self::$debug >= 2) {
                echo '[DM-DEBUG] Authentication failed..  Result:/n';
                print_r($command->result);
            }
            die("[".date('H:i:s')."|Dedimania] Error: Could not connect to Dedimania, account is not valid!\n");
        }
			
        // insert currently running challenge ...
        $this->onBeginMap($this->storage->currentMap, false, false);
    }
    
   	public function onTick() {
		if(time() > $this->last_players_update) {
			$call = new DedimaniaCall();
            
            if(!$this->getGameMode()) {
                if(self::$debug) $this->writeConsole('Unsupported GameMode...  Dedimania set to inactive.');
                return;
            }
			
			$params = array(
				$this->sessionId,
				(object)$this->getServerInfoArray(),
                (object)array
                (
                    'UId' => $this->storage->currentMap->uId, 
                    'GameMode' => $this->getGameMode()
                ),
				(object)$this->getPlayersArray()
			);
            
            if(self::$debug >= 2) {
                echo '[DM-DEBUG] Sending KeepAlive..  Params: ';
                print_r($params);
            }
			
			$call->addRequest(new Request('dedimania.UpdateServerPlayers', $params));
			
			$command = new RunCommand($call, array($this, 'cbTick'));
			ThreadPool::getInstance()->addCommand($command, $this->thread_id);
		
			$this->last_players_update = time() + self::$idleTimeout;
		}
    }
    
   	public function cbTick($command) {			
        if(isset($command->result[0]['errors']) && $command->result[0]['errors'] != '') {
            $this->writeConsole($command->result[0]['errors']);
        }
			
        if($command->result[0]['OK']) {
            $this->writeConsole('Successfully sent keep-alive to server!');
            if(self::$debug >= 3) {
                echo '[DM-DEBUG] Request Result:/n';
                print_r($command->result);
            }
        } else {
            //print_r($command->result);
            $this->writeConsole('ERROR: Could not send keep-alive!');
            if(self::$debug >= 2) {
                echo '[DM-DEBUG] Response:/n';
                print_r($command->result);
            }
        }
    }
    
	public function onPlayerConnect($login, $isSpectator) {
		if(!$this->is_LAN_login($login)) {
            if(self::$debug) $this->writeConsole('Skipping connection event of LAN player: '.$login);
            return;
		}
		
		$player = $this->storage->getPlayerObject($login);
		
		$call = new DedimaniaCall();
		
		$params = array
		(
			$this->sessionId,
			$player->login,
			$player->nickName,
			$player->path,
			($player->isSpectator != null && $player->isSpectator != false)
		);
        
        if(self::$debug >= 2) {
            echo '[DM-DEBUG] Sending Connected Player ('.$login.') info..  Params:/n';
            print_r($params);
        }
		
		$call->addRequest(new Request('dedimania.PlayerConnect', $params));
		
		$command = new RunCommand($call, array($this, 'cbPlayerConnect'));
		ThreadPool::getInstance()->addCommand($command, $this->thread_id);
	}
	
	public function cbPlayerConnect($command) {
		if($command->result[0]['OK']) {
			$this->writeConsole('Player ' . $command->result[0]['Login'] . ' has been marked online!');
            $this->last_players_update = time() + self::$idleTimeout;
            if(self::$debug >= 3) {
                echo '[DM-DEBUG] Request Result:/n';
                print_r($command->result);
            }
		} else {
            $this->writeConsole('Error!! Submitting connected player (' . $command->result[0]['Login'] . ') unsuccessful!');
            if(self::$debug >= 2) {
                echo '[DM-DEBUG] Response:/n';
                print_r($command->result);
            }
        }
        
        if(isset($command->result[0]['errors']) && $command->result[0]['errors'] != '') {
            $this->writeConsole($command->result[0]['errors']);
        }
	}
    
	public function onPlayerDisconnect($login) {		
		$call = new DedimaniaCall();
		
		$params = array
		(
			$this->sessionId,
			$login,
            ''
		);
        
        if(self::$debug >= 2) {
            echo '[DM-DEBUG] Sending Disconnected Player ('.$login.') info..  Params:/n';
            print_r($params);
        }
		
		$call->addRequest(new Request('dedimania.PlayerDisconnect', $params));
		
		$command = new RunCommand($call, array($this, 'cbPlayerDisconnect'));
		ThreadPool::getInstance()->addCommand($command, $this->thread_id);
	}
	
	public function cbPlayerDisconnect($command) {	
		if($command->result[0]['OK']) {
			$this->writeConsole('Player ' . $command->result[0]['Login'] . ' has been marked offline!');
            $this->last_players_update = time() + self::$idleTimeout;
            if(self::$debug >= 3) {
                echo '[DM-DEBUG] Request Result:/n';
                print_r($command->result);
            }
        } else {
            $this->writeConsole('Error!! Submitting disconnected player (' . $command->result[0]['Login'] . ') unsuccessful!');
            if(self::$debug >= 2) {
                echo '[DM-DEBUG] Response:/n';
                print_r($command->result);
            }
		}
        
        if(isset($command->result[0]['errors']) && $command->result[0]['errors'] != '') {
            $this->writeConsole($command->result[0]['errors']);
        }
	}
    
    public function onBeginMap($map, $WarmUp = false, $MatchContinuation = false) {
		// reset and busy ...
		$this->records = null;
        $this->records_dedi = null;
		$this->records_last = null;
		$this->records_new = null;
		$this->setBusy();
		
		if($map == null) {
			return;
		} elseif(is_array($map)) {
			$map = \ManiaLive\DedicatedApi\Structures\Map::fromArray($map);
        }
        
        if(!$this->getGameMode()) {
            if(self::$debug) $this->writeConsole('Unsupported GameMode...  Dedimania set to inactive.');
            return;
        }
		
		$this->last_players_update = time() + self::$idleTimeout;
		
		$call = new DedimaniaCall();

		$params = array
        (
            $this->sessionId,
            (object)array
            (
                'UId' => $map->uId,
                'Name' => $map->name,
                'Environment' => $map->environnement,
                'Author' => $map->author,
                'NbCheckpoints' => $map->nbCheckpoints,
                'NbLaps' => $map->nbLaps
            ),
            $this->getGameMode(),
            $this->getServerInfoArray(),
            $this->getPlayersArray()
        );
        
        if(self::$debug) $this->writeConsole('Requesting Challenge Records From Dedimania..');
        if(self::$debug >= 2) {
            echo '[DM-DEBUG] Request:/n';
            print_r($params);
        }
        
		$call->addRequest(new Request('dedimania.GetChallengeRecords', $params));
		
		// creating command and send it to a process ...
		$command = new RunCommand($call, array($this, 'cbBeginMap'));
		ThreadPool::getInstance()->addCommand($command, $this->thread_id);
	}
	
	public function cbBeginMap($command) {
		if(!$command->result[0]['OK']) {
            $this->writeConsole('Error!! Retrieving Records List from Dedimania unsuccessful!');
            if(self::$debug >= 2) {
                echo '[DM-DEBUG] Response:/n';
                print_r($command->result);
                return;
            }
			$this->writeConsole($command->result[0]['Error']['Message']);
		} else {
            if(self::$debug) $this->writeConsole('Successfully retrieved Records List from Dedimania.');
            $this->last_players_update = time() + self::$idleTimeout;
            if(self::$debug >= 3) {
                echo '[DM-DEBUG] Request Result:/n';
                print_r($command->result);
            }
		}
        
        if(isset($command->result[0]['errors']) && $command->result[0]['errors'] != '') {
            $this->writeConsole($command->result[0]['errors']);
        }
		
		// parse challenge object from response ...
		$challenge = ($command->result[0]);
		
		$this->map_current = $challenge;
        
        if(self::$debug >= 2) {
            echo '[DM-DEBUG] Retrieved Challenge Info:/n';
            print_r($challenge);
        }
		//var_dump($challenge);
		// records to the current challenge ...
		$this->records_dedi = $challenge;
        unset($this->records);
        $this->records = array();
        $i = 0;
        foreach($challenge['Records'] as $record) {
            $this->records[$i] = new Record();
            $this->records[$i]->login = $record['Login'];
            $this->records[$i]->rank = $i+1;
            $this->records[$i]->nickName = $record['NickName'];
            $this->records[$i]->best = $record['Best'];
            $this->records[$i]->score = $record['Best'];
            $this->records[$i]->MaxRank = $record['MaxRank'];
            $this->records[$i]->checks = $record['Checks'];
            $this->records[$i]->vote = $record['Vote'];
            $this->records[$i]->challenge = $challenge;
            $i++;
        }
        $this->records_max = $challenge['ServerMaxRank'];
		
		// records organized by logins ...
		$this->records_by_login = array();
		foreach($this->records as $record) {
			$this->records_by_login[$record->login] = $record;
		}
		
		// how many records are there in total ...
		$this->records_count = count($this->records);
		
		// set link to the last record on the leaderboard ...
		if($this->records_count > 0) {
			$this->records_last = $this->records[count($challenge['Records'])-1];
		} else {
			$this->records_last = null;
        }
			
		// records that will be new inserted ...
		$this->records_new = array();
		
		if(empty($this->records)){
            // loaded records for that track!
            $this->writeConsole('No records for: ' . $challenge['UId']);
            $this->connection->chatSendServerMessage('$fff»»$093This track does not have any Dedimania Records yet!');
		} else {
            // loaded records for that track!
            $this->writeConsole('Got records for: ' . $challenge['UId']);
            $this->connection->chatSendServerMessage('$fff»»$093Dedimania record on $fff$l[http://dedimania.net/tm2stats/?do=stat&UId='.$challenge['UId'].'&Show=RECORDS]'.$this->storage->currentMap->name.'$l$z$s$093 is driven by $fff$l[http://dedimania.net/tm2stats/?do=stat&Login='.$this->records[0]->login.'&Show=RECORDS]'.$this->records[0]->nickName.'$l$z$s$093 with a time of $fff'.Time::fromTM($this->records[0]->best).'$093!');
        }
        
        // dedimania is ready, we can receive records!
        $this->setReady();
        
        Dispatcher::dispatch(new onDataSync($this->records));
	}
	
	public function onEndMap($rankings, $map, $wasWarmUp, $matchContinuesOnNextMap, $restartMap){
		if(!$this->getGameMode()) {
            if (self::$debug >= 2) $this->writeConsole('Unsupported GameMode...  Dedimania still inactive.');
            return;
        }
        
		if (self::$debug) $this->writeConsole('Map Ended trying to send data towards Dedimania');
    
		// check if there are any records to send
		if(!is_array($this->records_new) || empty($this->records_new)) {
			if (self::$debug) $this->writeConsole('[DM-DEBUG] No new records to send!');
			return;
		}
        
		if(isset($this->records_dedi['UId']) && isset($this->records_dedi['OK']) && $this->records_dedi['OK']) {
			if($this->records_dedi['OK'] && !empty($this->records_new)) {
				$dataDir = $this->connection->gameDataDirectory();
				$dataDir = str_replace('\\', '/', $dataDir);
        
				$replaydir = $dataDir . "Replays/";
        
				//print_r($replaydir);
				// build times array
                if (self::$debug) $this->writeConsole('[DM-DEBUG] Building New Records Array..');
				$times = array();
				$logins = array();
				foreach($this->records_new as $record) {
					$times[] = array
                    (
                        'Login' => $record->login,
                        'Best' => $record->best,
                        'Checks' => implode(',', $record->checks)
                    );
				}
       	
				$first_time_ok = false;
        
				while(!$first_time_ok && !empty($times)) {
				    if(self::$debug) $this->writeConsole('[DM-DEBUG] Building Replays Array..');
					$replays = array();
          
					$vreplay = $this->connection->getValidationReplay($times[0]['Login']);
					$vrdata = $this->getGbxXmlData($vreplay);
					//print_r($vrdata);
					if(!$vrdata || !isset($vrdata['header']['.attr.map']['uid'])) {
						if(self::$debug) $this->writeConsole('[DM-DEBUG] Check failed: "!$vrdata || !isset($vrdata[\'header\'][\'.attr.map\'][\'uid\'])"...!');
						return false;
					}
          
					// checks VR uid
					if($vrdata['header']['.attr.map']['uid'] != $this->storage->currentMap->uId) {
						if(self::$debug) $this->writeConsole('[DEBUG] Check failed: "$vrdata[\'header\'][\'.attr.map\'][\'uid\'] != $this->storage->currentMap->uId"...!');
						return 'BADUID';
					}
          
					if(self::$debug) $this->writeConsole('[DM-DEBUG] VReplay passed checks!');
          
					$rfile = sprintf('replay.%s.%d.%07d.%s.Replay.Gbx',
													 $this->storage->currentMap->uId,
													 $this->storage->gameInfos->gameMode,
													 $times[0]['Best'],$times[0]['Login']);
					$ghostreplay = $this->connection->SaveBestGhostsReplay($times[0]['Login'], $rfile);
					if($greplay = file_get_contents($replaydir.$rfile)){
						// theorically here the ghost replay xml should be tested like for the validation replay
						if(self::$debug) $this->writeConsole('[DEBUG] Got GReplay...!');	
					} else {
						$this->writeConsole('DM.getGhostReplay:: failed to read '.$rfile.' probably bad character in path, bad file permissions, or tried to use the script remotely ?!');
						// if can't read the GhostReaply file then no reason to think that next will be able to : send nothing
						return 'FAILED';
					}
          
					//print_r($greplay);
					$frfile = $replaydir.$rfile;
					//print_r($frfile);
					$grdata = $this->getGbxXmlData($greplay);
          
					$vreplay = new Base64($vreplay);
					$top1greplay = new Base64($greplay);
					$replays = array
                    (
                        'VReplay' => $vreplay, 
                        'VReplayChecks' => $times[0]['Checks'],
                        'Top1GReplay' => $top1greplay
				    );
                    
					$first_time_ok = true;
					
					//$this->writeConsole('[DEBUG] VReplay: '.$vreplay->getXml());
					//$this->writeConsole('[DEBUG] VReplayChecks: '.$times[0]['Checks']);
					//$this->writeConsole('[DEBUG] Top1GReplay: '.$top1greplay->getXml());
					
					$call = new DedimaniaCall();
					// Uid, Name, Environment, Author, Game, Mode, NumberOfChecks, MaxGetTimes, Times
					$params = array
                    (
                        $this->sessionId,
                        array
                        (
                            'UId' => $this->storage->currentMap->uId,
                            'Name' => $this->storage->currentMap->name,
                            'Environment' => $this->storage->currentMap->environnement,
                            'Author' => $this->storage->currentMap->author,
                            'NbCheckpoints' => $this->storage->currentMap->nbCheckpoints,
                            'NbLaps' => $this->storage->currentMap->nbLaps
                        ),
                        $this->getGameMode(),
                        $times,
                        $replays
                    );
					//print_r($params);
                    if(self::$debug) $this->writeConsole('..Sending Request to Dedimania..');
                    if(self::$debug >= 3) {
                        echo '[DM-DEBUG] Request Data:/n';
                        print_r($params);
                    }
          
					$call->addRequest(new Request('dedimania.SetChallengeTimes', $params));
          
					// creating command and send it to a process ...
					$command = new RunCommand($call, array($this, 'cbEndMap'));
					ThreadPool::getInstance()->addCommand($command, $this->thread_id);
				}
			}	
		}	
	}
		
	
	public function cbEndMap($command) {	
		if(isset($command->result[0]['Error']['Message']) && $command->result[0]['Error']['Message'] != '') {
			$this->writeConsole($command->result[0]['Error']['Message']);
		} elseif(isset($command->result[0]['errors']) && $command->result[0]['errors'] != '') {
			$this->writeConsole($command->result[0]['errors']);
		} elseif($command->result[0]['OK']) {
			$this->last_players_update = time() + self::$idleTimeout;
            if(self::$debug >= 3) {
                echo '[DM-DEBUG] Request Result:/n';
                print_r($command->result);
            }
		}
		
		$challenge = Map::fromArray($command->result[0]);
		$this->writeConsole('Inserted records for: ' . $challenge->uId);
	}
    
    /**
     * Get the current GameMode, return either 'TA' or 'Rounds'
     */
    protected function getGameMode() {
        $gamemode = $this->storage->gameInfos->gameMode;
        if($gamemode == 0 | $gamemode == 6) {            // 0 = Script, 6 = Stunts;neither of which are supported
            if(self::$debug) $this->writeConsole('Gamemode '.$gamemode.' is not supported!');
            return '';
        } elseif($gamemode == 2 | $gamemode == 4) {      // 2 = TimeAttack, 4 = Laps
            return 'TA';
        } else {
            return 'Rounds';                            // 1 = Rounds, 3 = Team, 5 = Cup
        }
    }

	/*
		get data from xml of replay gbx
	*/
	function getGbxXmlData($rawgbx){
		$datas = false;
		$startxml = strpos($rawgbx,'<header type=');
		$endxml = strpos($rawgbx,'</header>');
		if($startxml !== false && $endxml !== false && $startxml < $endxml){
			$xml = substr($rawgbx,$startxml,$endxml + 9 - $startxml);
			//console("gbxxml: $xml");
			$datas = $this->xml_parse_string($xml);
			//console("gbxdata: ".print_r($datas,true));
		}
		return $datas;
	}

    
   	public function onPlayerCheckpoint($playerUid, $login, $timeOrScore, $curLap, $checkpointIndex) {
        if ((($checkpointIndex + 1) % $this->storage->currentMap->nbCheckpoints) == 0) {
            return;
        }
	}
    
    public function onPlayerFinish($playerUid, $login, $timeOrScore) {
		// stunts is currently not implemented
		// and laps is implemented in onplayerfinishlap
		if($this->storage->gameInfos->gameMode == GameInfos::GAMEMODE_STUNTS || $this->storage->gameInfos->gameMode == GameInfos::GAMEMODE_LAPS) {
			return;
		}

		// if first record is driven during load, then insert into queue and
		// process later as soon as ready!
		if(!$this->isReady()) {
			$callback = array($this, 'onPlayerFinish');
			$this->registerWaitForReady($callback, $playerUid, $login, $timeOrScore);
			return;
		}
		
		// if no actual finish, bail out immediately
		if ($timeOrScore == 0) return;
		
	   $player = $this->storage->getPlayerObject($login);
		// create record object ..
		$record = new Record();
		$record->best = $player->bestTime;
        $record->score = $player->bestTime;
		$record->checks = $player->bestCheckpoints;
		$record->login = $player->login;
		$record->nickName = $player->nickName;
		$record->challenge = $this->map_current;
		Console::printDebug($player->login . ' drove new best!');
    
		$this->insertRecord($player, $record);
        
        Dispatcher::dispatch(new onDataSync($this->records));
	}

	/**
	 *
	 * Enter description here ...
	 * @param $player
	 * @param $record
	 */
     
	protected function insertRecord($player, $record) {
		// validate record
		if(!$record->validate()) {
			return;
		}
		// is this relevant for best times:
		// either the player's time is at least better than the last record or there are no more
		// than 30 records yet.
		if ($this->records_last == null
			|| $player->bestTime < $this->records_last->best
			|| $this->records_max < 30)
		{
			// this is the first record of this player ...
			if(!array_key_exists($record->login, $this->records_by_login)) {
				// insert new record to the end of the list ...
				$record->rank = $this->records_count + 1;
				$this->records[$this->records_count] = $record;
				$this->records_count++;
				
				// this player has a record now!
				$this->records_by_login[$record->login] = $record;
				// shift the record up until it is on the right position ...
				for($i = $this->records_count - 1; $i > 0; $i--) {
					if ($this->records[$i]->best < $this->records[$i-1]->best)
						$this->swapRecords($i, $i-1);
					else
						break;
				}

				// player drove first record ...
				$this->records_new[] = $record;

				// send notification to all players
				if(self::$notifications) {
					if($record->rank == 1)	{
						$msg = $this->prepareMessage($record, self::$notifyNewFirstRecord);
						$this->connection->chatSendServerMessage($msg);
					} else {
						$msg = $this->prepareMessage($record, self::$notifyNewRecord);
						$this->connection->chatSendServerMessage($msg);
					}
				}
				$this->writeConsole($record->login . ' drove a new ' . $record->rank . '. record!');

			// this player has a record already!
			} else {
				// this time is worse than player's current record ...
				if($this->records_by_login[$record->login]->best <= $record->best) return;

				// search for the old record in the leaderboard ...
				$old_rank = 0;
				for($i = $this->records_count - 1; $i >= 0; $i--) {
					// if we found the old record, then update it
					if($this->records[$i]->login == $record->login) {
						$this->records[$i]->best = $record->best;
                        $this->records[$i]->rank = $i+1;
                        $this->records[$i]->score = $record->score;
						$this->records[$i]->checks = $record->checks;
						$record = $this->records[$i];
						$old_rank = $i;
						//print_r($this->records);
						// move record forward ...
						for ($n = $i; $n > 0; $n--) {
							if ($this->records[$n]->best < $this->records[$n-1]->best)
								$this->swapRecords($n, $n-1);
							else
								break;
						}
						break;
					}
				}

				// send notification to all players
				if(self::$notifications) {
					if($record->rank == 1) {
						if($old_rank == 0) {
							$msg = $this->prepareMessage($record, self::$notifyImprovedFirstRecord);
							$this->connection->chatSendServerMessage($msg);
						} else {
							$msg = $this->prepareMessage($record, self::$notifyNewFirstRecord);
							$this->connection->chatSendServerMessage($msg);
						}
					} else {
						if($record->rank != ($old_rank+1)) {
							$msg = $this->prepareMessage($record, self::$notifyImprovedRecord);
							$this->connection->chatSendServerMessage($msg);
						} else {
							$msg = $this->prepareMessage($record, self::$notifyImprovedRecordTimeOnly);
							$this->connection->chatSendServerMessage($msg);
						}
					}
				}

				// player improved his record ...
				$this->records_new[] = $record;
				$this->writeConsole($record->login.' improved to ' . $record->rank . '. rank and was ' . ($old_rank+1) . '. before!');
			}

			}

			// remove all records that are redundant
			while ($this->records_count > 30)
			{
				$record = array_pop($this->records);
				if (isset($this->records_by_login[$record->login]))
				{
					unset($this->records_by_login[$record->login]);
					$this->records_count--;
				}
				$record->destroy();
			}

			// point to the last element of all records
			$this->records_last = $this->records[$this->records_count - 1];
        
        Dispatcher::dispatch(new onDataSync($this->records));
	}	
	/**
	 * Replace certain keywords in the message that
	 * can be defined in the config file.
	 * @param Record $record
	 * @param string $message
	 */
	protected function prepareMessage(Record $record, $message) {
		$search = array(
			'%player%',
			'%time%',
			'%rank%'
		);
		
		$replace = array(
			$record->nickName,
			Time::fromTM($record->best),
			$record->rank.'.'
		);
		
		return str_replace($search, $replace, $message);
	}
	
	/**
	 * Swaps to records in the ranking table
	 * if they are in wrong order.
	 * @param $i
	 * @param $n
	 */
	protected function swapRecords($i, $n) {
		$temp = $this->records[$n];
		$this->records[$n] = $this->records[$i];
		$this->records[$i] = $temp;
		$this->records[$i]->rank = $i+1;
		$this->records[$n]->rank = $n+1;
	}
    
   	/**
	 * Get an array with infromation needed by
	 * Dedimania for a player.
	 */
	protected function getPlayersArray() {
		$players = array();
		
		// parse players ...
		foreach($this->storage->players as $player) {
			$players[] = array(
				'Login' => $player->login,
				'Nation' => $player->path,
				'TeamId' => $player->teamId,
				'IsSpec' => false,
				'Ranking' => $player->ladderRanking,
				'IsOff' => $player->isInOfficialMode
			);
		}
		
		// parse spectators ...
		foreach ($this->storage->spectators as $player)	{
			$players[] = array(
				'Login' => $player->login,
				'Nation' => $player->path,
				'TeamId' => $player->teamId,
				'IsSpec' => true,
				'Ranking' => $player->ladderRanking,
				'IsOff' => $player->isInOfficialMode
			);
		}
		return $players;
	}
	
	/**
	 * Get an array with information about the server
	 * that is needed by Dedimania.
	 */
   	protected function getServerInfoArray() {
		return array(
			'SrvName' => $this->storage->server->name,
			'Comment' => $this->storage->server->comment,
			'Private' => isset($this->server->password),
			'SrvIP' => '178.239.60.101',
			'SrvPort' => 2350,
			'XmlrpcPort' => 5000,
			'NumPlayers' => count($this->storage->players),
			'MaxPlayers' => $this->storage->server->currentMaxPlayers,
			'NumSpecs' => count($this->storage->spectators),
			'MaxSpecs' => $this->storage->server->currentMaxSpectators,
			'LadderMode' => $this->storage->server->currentLadderMode
		);
	}
    
   	/**
	 * Register a callback method that is
	 * executed as soon as the server is ready.
	 */
	public function registerWaitForReady() {
		$args = func_get_args();
		$callback = array_shift($args);
		
		$this->callback_queue[] = array
		(
			'callback' => $callback,
			'params' => $args
		);
	}
	
	/**
	 * Is Dedimania currently loading?
	 * @return bool
	 */
	public function isReady() {
		return $this->ready;
	}
	
	/**
	 * Dedimania starts to load records.
	 */
	protected function setBusy() {
		$this->ready = false;
	}
	
	/**
	 * Dedimania has finished loading.
	 * Inform everyone that has subscribed!
	 */
	protected function setReady() {
		$this->ready = true;
		
		while($entry = array_shift($this->callback_queue))	{
			if(is_callable($entry['callback'])) {
				call_user_func_array($entry['callback'], $entry['params']);
			}
		}
	}
    
    public function getRecords() {
        return $this->records;
    }
	
	/*
	 * Helper functions for Dedimania
	 */

    // parse xml string to assossiative array
    // (array built with attrmix mode will not be able to be build in the same xml)
    function xml_parse_string($xmlstr, $attrmix=false){
        global $_xml_parser_values;
        $res = array();

        if(strlen($xmlstr) > 5) {
            $parser = xml_parser_create('UTF-8');
            xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, 0);
            xml_parser_set_option($parser, XML_OPTION_SKIP_WHITE, 0);
            xml_parser_set_option($parser, XML_OPTION_TARGET_ENCODING, 'UTF-8');
            xml_parse_into_struct($parser, $xmlstr, $_xml_parser_values,$tags);
            xml_parser_free($parser);
            //debugPrint("xml_parse_string - _xml_parser_values",$_xml_parser_values);
            //debugPrint("xml_parse_string - tags",$tags);
            $res = $this->xml_parser_buildarray(reset($_xml_parser_values), $attrmix);
        }
        return $res;
    }

    // parse xml file to assossiative array
    // (array built with attrmix mode will not be able to be build in the same xml)
    function xml_parse_file($xmlfile, $attrmix=false, $errormsg=false){
        $res = array();
        if(file_exists($xmlfile)) {
            $xmlstr = file_get_contents($xmlfile);
            $res = $this->xml_parse_string($xmlstr, $attrmix);
        } else {
            echo "xml_parse_file: can't find file $xmlfile.\n";
            return false;
        }
        return $res;
    }


    // make a xml string from an associative array
    function xml_build($datas){
        global $_xml_parser_tabs;
        $_xml_parser_tabs = "\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t";

        $res = '<?xml version="1.0" encoding="UTF-8" ?>';
        $res .= $this->xml_parser_buildxml($datas, 0);

        return $res;
    }


    // build xml string from an associative array and save to file
    function xml_build_to_file($datas, $xmlfile){
        return file_put_contents($xmlfile, $this->xml_build($datas));
    }

    function xml_parser_buildarray($tag, $attrmix=false){
        global $_xml_parser_values;
        $res = array();
        $tsub = array();

        for(; $tag !== false && $tag['type'] != 'close'; $tag = next($_xml_parser_values)) {
            $tagval = '';
            $tagattr = null;
            $tname = $tag['tag'];

            if($tag['type'] == 'complete' || $tag['type'] == 'open') {
                if(isset($tag['attributes'])) {
                    if($attrmix) {
                        // attribute in attrmix mode : the key take the attr array
                        $tagval = $tag['attributes'];
                    } else {
                        // attribute in normal mode : add a special attr key
                        $tagattr = $tag['attributes'];
                    }
                }

                if($tag['type'] == 'complete') {
                    // final value
                    if(isset($tag['value'])){
                        if($attrmix && isset($tag['attributes'])) {
                            // final key with value and attr in attrmix mode
                            $tagval['value'] = $tag['value'];
                        } else {
                            // final key without attrmix mode
                            $tagval = $tag['value'];
                        }
                    }

                } elseif($tag['type'] == 'open') {
                    // array key
                    $tagval = $this->xml_parser_buildarray(next($_xml_parser_values),$attrmix);
                }

                // put $tagval and $tagattr in array...

                if(isset($res[$tname])) {
                    if($attrmix && is_array($res[$tname])) {
                        // key already exist, is an array, and attrmix mode : just  complete the array
                        $res[$tname] = array_merge($res[$tname],$tagval);
                    } else {
                        // key already exist : make multi same key array (add '.xml_parser_built.'=>true in array to remind it)
                        if(!isset($tsub[$tname])) {
                            // first time : make the array
                            $tsub[$tname] = 0;
                            if(isset($res['.attr.'.$tname]))
                                $res['.attr.'.$tname] = array($res['.attr.'.$tname]);
                            $tmp = $res[$tname];
                            $res[$tname] = array('.multi_same_tag.'=>true);
                            $res[$tname][$tsub[$tname]++] = $tmp;
                        }
                        if($tagattr !== null) {
                            if(!isset($res['.attr.'.$tname])) {
                                $res['.attr.'.$tname] = array();
                                $tmp = $res[$tname];
                                unset($res[$tname]);
                                $res[$tname] = $tmp;
                            }
                            $res['.attr.'.$tname][$tsub[$tname]] = $tagattr;
                        }
                        $res[$tname][$tsub[$tname]++] = $tagval;
                    }

                } else {
                    // put normal result in the given array
                    if($tagattr !== null)
                        $res['.attr.'.$tname] = $tagattr;
                    $res[$tname] = $tagval;
                }
            }

        }
        return $res;
    }

    function xml_parser_buildxml(&$tval, $level) {
        global $_xml_parser_tabs,$_xml_parser_sep;
        $res = '';
        if(is_array($tval) && count($tval) > 0) {
            foreach($tval as $tag => $val) {
                if(strncmp($tag,'.attr.',6) == 0) {
                    // attribute: do nothing
                } elseif(is_array($val) && isset($val['.multi_same_tag.']) && $val['.multi_same_tag.']) {
                    // multi same key array
                    for($i=0; $i < count($val)-1; $i++) {
                        if(isset($val[$i])) {
                            $tattrs = $this->xml_parser_buildxml_tagattrs($tval, $tag, $i);
                            $res2 = $this->xml_parser_buildxml($val[$i], $level+1);
                            if($res2 == '' && $tattrs != '') {
                                // if empty with attrs then make auto close tag
                                $res .= "\n".substr($_xml_parser_tabs, 0, $level).'<'.$tag.$tattrs.' />';
                            } else {
                                $res .= "\n".substr($_xml_parser_tabs, 0, $level).'<'.$tag.$tattrs.'>'.$res2."</$tag>";
                            }
                        }
                    }
                } else {
                    // classig tag
                    $tattrs = $this->xml_parser_buildxml_tagattrs($tval, $tag, false);
                    $res2 = $this->xml_parser_buildxml($val, $level+1);
                    if($res2 == '' && $tattrs != '') {
                        // if empty with attrs then make auto close tag
                        $res .= "\n".substr($_xml_parser_tabs, 0, $level).'<'.$tag.$tattrs.' />';
                    } else {
                        $res .= "\n".substr($_xml_parser_tabs, 0, $level).'<'.$tag.$tattrs.'>'.$res2."</$tag>";
                    }
                }
            }
            $res .= "\n".substr($_xml_parser_tabs, 0, $level-1);

        } else {
            $res .= $tval;
        }
        return $res;
    }


    function xml_parser_buildxml_tagattrs(&$attrs, $tag, $index=false){
        $res = '';
        if($index === false){
            if(isset($attrs['.attr.'.$tag]) && is_array($attrs['.attr.'.$tag])) {
                foreach($attrs['.attr.'.$tag] as $key => $val) {
                    $res .= " $key=\"$val\"";
                }
            }
        } else {
            if(isset($attrs['.attr.'.$tag][$index]) && is_array($attrs['.attr.'.$tag][$index])) {
                foreach($attrs['.attr.'.$tag][$index] as $key => $val) {
                    $res .= " $key=\"$val\"";
                }
            }
        }
        return $res;
    }

    // urlsafe base64 alternative encode
    function urlsafe_base64_encode($string) {
        $data = base64_encode($string);
        $data = str_replace(array('+','/','='), array('-','_',''), $data);
        return $data;
    }  // urlsafe_base64_encode

    // urlsafe base64 alternative decode
    function urlsafe_base64_decode($string) {

        $data = str_replace(array('-','_'), array('+','/'), $string);
        $mod4 = strlen($data) % 4;
        if ($mod4) {
            $data .= substr('====', $mod4);
        }
        return base64_decode($data);
    }  // urlsafe_base64_decode

	// threading listener
	//function onThreadStart($thread) {}
	//function onThreadDies($thread) {}
	//function onThreadTimesOut($thread) {}
	
	// dedimania listener
	function onPlayerNewRank($player, $rank_old, $rank_new) {}
	function onPlayerNewBestScore($player, $score_old, $score_new) {}
}

// exception classes
class Exception extends \Exception {}
class ConfigurationException extends Exception {}
class NotConnectedException extends Exception {}
?>